這可能會是個很多人覺得沒有太大意義的題目,就是在 Windows 上最小的執行檔可以有多小?我們在追求執行檔最小化的同時,還必須滿足這隻執行檔要能夠將 “Hello World!” 印在 console。
在 C 的程式碼會是這樣:
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}
在 Visual Studio 2019 編譯完後是 10.5KB
載入了許多 DLL ,同時也有許多 section
接著,我們來看看到主要的 printf 函數究竟會如何被執行,在此之前要先知道程式的 EntryPoint 會是哪裡
從 AddressOfEntryPoint 和 ImageBase ,可以知道程式執行的起始點會是 0x140001320
從 IDA View 的 proximity Browser 觀察 function 間的上下關係
從 ProcessExplorer 的 Call Stack 也可以得到 Runtime 期間 function 的 caller 關係
程式執行上會是: mainCRTStartup
→ _scrt_common_main_seh
→ main
,中間因為 Visual Studio 在編譯 PE 時產生 C Runtime (CRT) 占用了許多空間,並且初始化了一些程式可能需要的功能,像是 Exception Handler、Stack Guard、 Control Flow Guard 等功能,即使有些能透過設定 compiler 的參數關閉,但是只是單純的要執行 printf 應該可以節省更多空間,因此我做了一些研究看看其他人都是如何製作最小的 PE。
printf 是 CRT 的一部份,所以我們可以使用 Kernel32.dll 的 API WriteConsoleA 來取代 printf。此外,還必須要透過 GetStdHandle 取得當前 Process console 的 std output 的 handle,才能將 string 寫到 console
#include <Windows.h>
int main() {
char text[] = "Hello, World!\n";
::WriteConsoleA(::GetStdHandle(STD_OUTPUT_HANDLE),
text, (DWORD)strlen(text), nullptr, nullptr);
return 0;
}
因此為了讓 PE 盡可能的小,我們不能有 CRT,同時我們還希望能只載入必要的 DLL。
既不能有 CRT 又只能載入必要 DLL 的條件下,我們該怎麼做?從這個專案,我們可以看到作者異常生猛的直接將整個 PE File 寫成 asm。看起來太厲害了,所以我也決定如法炮製 XD
首先,作者提供了一組 compiler 參數,主要是將一些保護關閉、合併 sections 並且只載入必要的 DLL,像是 Kernel32.dll
& 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\cl.exe' /O1 /MD /GS- /source-charset:utf-8 tiny_pe.cpp /link /NOLOGO /NODEFAULTLIB /SUBSYSTEM:CONSOLE /ENTRY:main /MERGE:.rdata=. /MERGE:.pdata=. /MERGE:.text=. /SECTION:.,ER /ALIGN:16 'C:\Program Files (x86)\Windows Kits\10\lib\10.0.19041.0\um\x64\kernel32.Lib'
這些操作確實能大量的減少 binary 的空間,從 10.5 KB 直接降到 1KB
實際執行 process 也能夠透過 WriteConsoleA 正常寫入 console
如果遇到無法寫入 console 的情況,可以先檢查 GetStdHandle 是否有執行成功。
根據 MSDN,GetStdHandle 要回傳不是 -1 或 0 的值才可以被當作可以使用的 Handle
在 GetStdHandle 執行後,檢查回傳值在 RAX 是否有非零且正的 Handle 值
接下來就是最 hardcore 的部分了,這邊先將 PE File 轉換成 asm
BITS 64
%define align(n,r) (((n+(r-1))/r)*r)
; DOS Header
dw 'MZ' ; e_magic
dw 0 ; [UNUSED] e_cblp
dw 0 ; [UNUSED] c_cp
dw 0 ; [UNUSED] e_crlc
dw 0 ; [UNUSED] e_cparhdr
dw 0 ; [UNUSED] e_minalloc
dw 0 ; [UNUSED] e_maxalloc
dw 0 ; [UNUSED] e_ss
dw 0 ; [UNUSED] e_sp
dw 0 ; [UNUSED] e_csum
dw 0 ; [UNUSED] e_ip
dw 0 ; [UNUSED] e_cs
dw 0 ; [UNUSED] e_lfarlc
dw 0 ; [UNUSED] e_ovno
times 4 dw 0 ; [UNUSED] e_res
dw 0 ; [UNUSED] e_oemid
dw 0 ; [UNUSED] e_oeminfo
times 10 dw 0 ; [UNUSED] e_res2
dd pe_hdr ; e_lfanew
; DOS Stub
times 8 dq 0 ; [UNUSED] DOS Stub
; Rich Header
times 8 dq 0 ; [UNUSED] Rich Header
; PE Header
pe_hdr:
dw 'PE', 0 ; Signature
; Image File Header
dw 0x8664 ; Machine
dw 0x01 ; NumberOfSections
dd 0 ; [UNUSED] TimeDateStamp
dd 0 ; PointerToSymbolTable
dd 0 ; NumberOfSymbols
dw opt_hdr_size ; SizeOfOptionalHeader
dw 0x22 ; Characteristics
; Optional Header, COFF Standard Fields
opt_hdr:
dw 0x020b ; Magic (PE32+)
db 0x0e ; MajorLinkerVersion
db 0x16 ; MinorLinkerVersion
dd code_size ; SizeOfCode
dd 0 ; SizeOfInitializedData
dd 0 ; SizeOfUninitializedData
dd entry ; AddressOfEntryPoint
dd iatbl ; BaseOfCode
; Optional Header, NT Additional Fields
dq 0x000140000000 ; ImageBase
dd 0x10 ; SectionAlignment
dd 0x10 ; FileAlignment
dw 0x06 ; MajorOperatingSystemVersion
dw 0 ; MinorOperatingSystemVersion
dw 0 ; MajorImageVersion
dw 0 ; MinorImageVersion
dw 0x06 ; MajorSubsystemVersion
dw 0 ; MinorSubsystemVersion
dd 0 ; Reserved1
dd file_size ; SizeOfImage
dd hdr_size ; SizeOfHeaders
dd 0 ; CheckSum
dw 0x03 ; Subsystem (Windows Console)
dw 0x8160 ; DllCharacteristics
dq 0x100000 ; SizeOfStackReserve
dq 0x1000 ; SizeOfStackCommit
dq 0x100000 ; SizeOfHeapReserve
dq 0x1000 ; SizeOfHeapCommit
dd 0 ; LoaderFlags
dd 0x10 ; NumberOfRvaAndSizes
; Optional Header, Data Directories
dd 0 ; Export, RVA
dd 0 ; Export, Size
dd itbl ; Import, RVA
dd itbl_size ; Import, Size
dd 0 ; Resource, RVA
dd 0 ; Resource, Size
dd 0 ; Exception, RVA
dd 0 ; Exception, Size
dd 0 ; Certificate, RVA
dd 0 ; Certificate, Size
dd 0 ; Base Relocation, RVA
dd 0 ; Base Relocation, Size
dd 0 ; Debug, RVA
dd 0 ; Debug, Size
dd 0 ; Architecture, RVA
dd 0 ; Architecture, Size
dd 0 ; Global Ptr, RVA
dd 0 ; Global Ptr, Size
dd 0 ; TLS, RVA
dd 0 ; TLS, Size
dd 0 ; Load Config, RVA
dd 0 ; Load Config, Size
dd 0 ; Bound Import, RVA
dd 0 ; Bound Import, Size
dd iatbl ; IAT, RVA
dd iatbl_size ; IAT, Size
dd 0 ; Delay Import Descriptor, RVA
dd 0 ; Delay Import Descriptor, Size
dd 0 ; CLR Runtime Header, RVA
dd 0 ; CLR Runtime Header, Size
dd 0 ; Reserved, RVA
dd 0 ; Reserved, Size
opt_hdr_size equ $-opt_hdr
; Section Table
section_name db '.' ; Name
times 8-($-section_name) db 0
dd sect_size ; VirtualSize
dd iatbl ; VirtualAddress
dd code_size ; SizeOfRawData
dd iatbl ; PointerToRawData
dd 0 ; PointerToRelocations
dd 0 ; PointerToLinenumbers
dw 0 ; NumberOfRelocations
dw 0 ; NumberOfLinenumbers
dd 0x60000020 ; Characteristics
hdr_size equ $-$$
code:
; Import Address Directory
iatbl:
iatbl_GetStdHandle:
dq symbol_GetStdHandle
iatbl_WriteConsoleA:
dq symbol_WriteConsoleA
dq 0
iatbl_size equ $-iatbl
; Strings
content:
db 0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x57
db 0x6f,0x72,0x6c,0x64,0x21,0x0a,0x00,0x00 ; "Hello, World!\n"
; Debug Table
times 24 dq 0 ; [UNUSED] Debug Table
; Entry
entry:
mov ecx, 0FFFFFFF5h ; StdHandle
call [rel iatbl_GetStdHandle] ; GetStdHandle
xor r9d, r9d ; lpNumberOfCharsWritten
lea r8d, [r9+0Fh] ; nNumberOfCharsToWrite
lea rdx, [rel content] ; lpBuffer
mov rcx, rax ; hConsoleOutput
call [rel iatbl_WriteConsoleA] ; WriteConsoleA
ret
times align($-$$,16)-($-$$) db 0xcc
; Import Directory
itbl:
dq intbl ; OriginalFirstThunk
dd 0 ; TimeDateStamp
dd dll_name ; ForwarderChain
dd iatbl ; Name
dq 0 ; FirstThunk
times 3 dd 0
itbl_size equ $-itbl
; Import Name Table
intbl:
dq symbol_GetStdHandle
dq symbol_WriteConsoleA
dq 0
; Symbol
symbol_GetStdHandle:
dw 0x1234 ; [UNUSED] Function Order
db 'GetStdHandle', 0 ; Function Name
symbol_WriteConsoleA:
dw 0xabcd ; [UNUSED] Function Order
db 'WriteConsoleA', 0 ; Function Name
dll_name:
db 'KERNEL32.dll', 0
db 0
sect_size equ $-code
times align($-$$,16)-($-$$) db 0
code_size equ $-code
file_size equ $-$$
接著,透過 nasm 組譯
& 'C:\Program Files\NASM\nasm.exe' -f bin -o tiny_pe_asm.exe -l tiny_pe.lst tiny_pe.asm
我們再次的將大小縮小至 880 bytes
接著,照著文章的步驟將 Optional Header 和 Debug Table 等沒有使用到的部分刪掉,檔案大小縮小至 448 Bytes
下一步,是要製造 overlapped 的部分
BITS 64
; DOS Header
dw 'MZ' ; e_magic
dw 0 ; [UNUSED] e_cblp
pe_hdr: ; PE Header
dw 'PE' ; [UNUSED] c_cp ; Signature
dw 0 ; [UNUSED] e_crlc ; Signature (Cont)
; Image File Header
dw 0x8664 ; [UNUSED] e_cparhdr ; Machine
code:
dw 0x01 ; [UNUSED] e_minalloc ; NumberOfSections
times 14-($-code) db 0 ; [UNUSED] e_maxalloc ; [UNUSED] TimeDateStamp
; [UNUSED] e_ss ; [UNUSED] TimeDateStamp (Cont)
; [UNUSED] e_sp ; [UNUSED] PointerToSymbolTable
; [UNUSED] e_csum ; [UNUSED] PointerToSymbolTable (Cont)
; [UNUSED] e_ip ; [UNUSED] NumberOfSymbols
; [UNUSED] e_cs ; [UNUSED] NumberOfSymbols (Cont)
dw opt_hdr_size ; [UNUSED] e_lfarlc ; SizeOfOptionalHeader
dw 0x22 ; [UNUSED] e_ovno ; Characteristics
opt_hdr: ; Optional Header, COFF Standard Fields
dw 0x020b ; [UNUSED] e_res ; Magic (PE32+)
db 0 ; [UNUSED] e_res (Cont) ; [UNUSED] MajorLinkerVersion
db 0 ; [UNUSED] e_res (Cont) ; [UNUSED] MinorLinkerVersion
dd code_size ; [UNUSED] e_res (Cont) ; SizeOfCode
dw 0 ; [UNUSED] e_oemid ; [UNUSED] SizeOfInitializedData
dw 0 ; [UNUSED] e_oeminfo ; [UNUSED] SizeOfInitializedData (Cont)
dd 0 ; [UNUSED] e_res2 ; [UNUSED] SizeOfUninitializedData
dd entry ; [UNUSED] e_res2 (Cont) ; AddressOfEntryPoint
dd code ; [UNUSED] e_res2 (Cont) ; BaseOfCode
; Optional Header, NT Additional Fields
dq 0x000140000000 ; [UNUSED] e_res2 (Cont) ; ImageBase
dd pe_hdr ; e_lfanew ; [MODIFIED] SectionAlignment (0x10 -> 0x04)
dd 0x04 ; [MODIFIED] FileAlignment (0x10)
dw 0x06 ; [UNUSED] MajorOperatingSystemVersion
dw 0 ; [UNUSED] MinorOperatingSystemVersion
dw 0 ; [UNUSED] MajorImageVersion
dw 0 ; [UNUSED] MinorImageVersion
dw 0x06 ; MajorSubsystemVersion
dw 0 ; MinorSubsystemVersion
dd 0 ; [UNUSED] Reserved1
dd file_size ; SizeOfImage
dd hdr_size ; SizeOfHeaders
dd 0 ; [UNUSED] CheckSum
dw 0x03 ; Subsystem (Windows GUI)
dw 0x8160 ; DllCharacteristics
dq 0x100000 ; SizeOfStackReserve
dq 0x1000 ; SizeOfStackCommit
dq 0x100000 ; SizeOfHeapReserve
dq 0x1000 ; SizeOfHeapCommit
dd 0 ; LoaderFlags
dd 0x2 ; NumberOfRvaAndSizes
; Optional Header, Data Directories
dd 0 ; [UNUSED] Export, RVA
dd 0 ; [UNUSED] Export, Size
dd itbl ; Import, RVA
dd itbl_size ; Import, Size
opt_hdr_size equ $-opt_hdr
; Section Table
section_name db '.', 0 ; Name
times 8-($-section_name) db 0
dd sect_size ; VirtualSize
dd iatbl ; VirtualAddress
dd code_size ; SizeOfRawData
dd iatbl ; PointerToRawData
content: ; Strings
db 0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x57
db 0x6f,0x72,0x6c,0x64,0x21,0x0a,0x00,0x00
; [UNUSED] PointerToRelocations
; [UNUSED] PointerToLinenumbers
; [UNUSED] NumberOfRelocations
; [UNUSED] NumberOfLinenumbers
; [UNUSED] Characteristics
hdr_size equ $-$$
; Symbol
symbol_GetStdHandle:
dw 0x0294 ; [UNUSED] Function Order
db 'GetStdHandle', 0 ; Function Name
symbol_WriteConsoleA:
dw 0x0295 ; [UNUSED] Function Order
db 'WriteConsoleA', 0 ; Function Name
dll_name:
db 'KERNEL32.dll', 0
db 0
iatbl: ; Import Address Directory
iatbl_GetStdHandle:
dq symbol_GetStdHandle ; [USEDAFTERLOAD] DLLFuncEntry (GetStdHandle)
iatbl_WriteConsoleA:
dq symbol_WriteConsoleA ; [USEDAFTERLOAD] DLLFuncEntry (WriteConsoleA)
iatbl_size equ $-iatbl
; Entry
entry:
mov ecx, 0FFFFFFF5h ; StdHandle
call [rel iatbl_GetStdHandle] ; GetStdHandle
xor r9d, r9d ; lpNumberOfCharsWritten
lea r8d, [r9+0Fh] ; nNumberOfCharsToWrite
lea rdx, [rel content] ; lpBuffer
mov rcx, rax ; hConsoleOutput
jmp [rel iatbl_WriteConsoleA] ; WriteConsoleA
itbl: ; Import Directory
dq intbl ; OriginalFirstThunk
dd 0 ; [UNUSED] TimeDateStamp
dd dll_name ; ForwarderChain
dd iatbl ; Name
intbl: ; Import Name Table
dq symbol_GetStdHandle ; [UNUSED] FirstThunk ; Symbol
dq symbol_WriteConsoleA ; nullptr
dq 0
itbl_size equ $-itbl
sect_size equ $-code
code_size equ $-code
file_size equ $-$$
可以將一些 header 的結構重疊,這樣可以節省許多空間,大小也縮小到 335 Bytes。
最後一步,則是修改 assembly code,透過使用 short jump 再次縮小空間,但因為時間不太夠沒有繼續玩下去,也興趣的讀者也可以跟著這個 blog 繼續操作下去。另外如果結構控制得夠好,應該會有機會重疊出3層的結構,讓空間發揮到極致。
最後我想從大神 Pavel Yosifovich 的文章來總結製作最小的 PE File 背後代表的意義是什麼 ?
文章中提到其實在想辦法製造成最小的 PE 的同時,我們也是正在朝製作一個 Native Application 邁進。而 Native Application 是為了可以在開機階段 csrss.exe 還未啟動時就能夠被成功載入,也就是 PE 的執行不需要依賴 CRT。
關於如何製作 Native Application 可以去看他的 Youtube 影片 會有更詳細的介紹。
這裡我也提出一個我也不確定解答的問題:某個程式如果有能力在大部分程式還未執行前執行,這會不會也代表有機會繞過一些防毒軟體或偵測手段(?
下一篇,我將介紹 DLL Loading!